# coding: utf-8

from __future__ import print_function
import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))
    
logic_or = np.vectorize(lambda x, y: x or y)

class Logistic(object):
    
    def __init__(self, max_iter=10000, alpha=0.00001, threshold=0.5, debug=False):
        self.max_iter = max_iter
        self.alpha = alpha
        self.threshold = threshold
        self.debug = debug
        
    def fit(self, X, Y):
        X = np.asarray(X).astype(float)
        Y = np.asarray(Y).astype(float)
        assert(len(X) == len(Y) and \
               X.ndim == 2 and \
               Y.ndim == 1 and \
               logic_or(Y == 0, Y == 1).all())
        Y = Y[..., np.newaxis]
        
        self.ndata, self.nfeatures = X.shape
        self.losses = []
        
        # 梯度下降
        # theta = np.random.rand(self.nfeatures, 1) / 100
        theta = np.zeros((self.nfeatures, 1))
        
        for round in range(self.max_iter):
        
            # 正向传播
            Z = np.dot(X, theta)
            A = sigmoid(Z)
            J = - np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A)) / self.ndata
            
            if round % 1000 == 0:
                print('round: {0}, loss: {1}'.format(round, J))
            self.losses.append(J)
            
            # 反向传播
            dJ_dZ = (A - Y) / self.ndata
            dJ_dtheta = np.dot(X.T, dJ_dZ)
            
            last_theta = theta.copy()
            theta -= self.alpha * dJ_dtheta
            
            if self.debug:
                print('X:\n', X[:10])
                print('theta:\n', last_theta)
                print('Z:\n', Z[:10])
                print('A:\n', A[:10])
                print('dJ_dZ:\n', dJ_dZ[:10])
                print('dJ_dtheta:\n', dJ_dtheta)
                print('')
        # end for
            
        self.theta = theta
    
    def proba(self, X):
        
        Z = np.dot(X, self.theta)
        A = sigmoid(Z)
        
        return A.ravel()
        
    def predict(self, X):
        prob = self.proba(X)
        posi_class = prob > self.threshold
        prob[posi_class] = 1
        prob[~posi_class] = 0
        return prob
            